package admin.auth.controller;


import admin.auth.domain.dto.Oauth2TokenDto;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.AES;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.*;
import admin.auth.feishu.properties.AuthProperties;
import admin.common.constant.AuthConstant;
import admin.common.domain.req.Oauth2LoginReq;
import admin.common.utils.FieldUtil;
import admin.common.vo.ResponseVO;
import admin.redis.service.RedisService;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.security.KeyPair;
import java.security.interfaces.RSAPublicKey;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

/**
 * 自定义Oauth2获取令牌接口
 * Created by zgb on 2020/7/17.
 */
@Slf4j
@RestController
@Tag(name = "认证中心登录认证")
//且security识别不了server.servlet.context-path
@RequestMapping("/oauth")
public class OauthController {

    @Autowired
    private KeyPair keyPair;
    @Autowired
    private TokenEndpoint tokenEndpoint;
    @Autowired
    private TokenStore tokenStore;
    @Autowired
    private RedisService redisService;

    @Autowired
    private AuthProperties authProperties;

    @Value("${aes.key}")
    private String key;
    @Value("${aes.salt}")
    private String salt;

    private AES aes;

    @PostConstruct
    public void init() {
        // 构建
        aes = new AES("CBC", "PKCS7Padding",
                // 密钥，可以自定义
                key.getBytes(),
                // iv加盐，按照实际需求添加
                salt.getBytes());
    }


    /**
     * 作为认证入口，实际要每个客户端封装一个登录接口通过feign进来
     */
    @Operation(summary = "Oauth2获取token")
    @PostMapping(value = "/entry", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    public ResponseVO<Oauth2TokenDto> entry(@Schema(name = "Oauth2登录 参数对象") Oauth2LoginReq oauth2LoginReq) throws HttpRequestMethodNotSupportedException {
        //不使用PasswordEncoder自动加密，使用外置灵活加密
        oauth2LoginReq.setPasswordEncodeYn(oauth2LoginReq.getPasswordEncodeYn() == null ? false : oauth2LoginReq.getPasswordEncodeYn()) ;
        if (BooleanUtil.isFalse(oauth2LoginReq.getPasswordEncodeYn())) {
            oauth2LoginReq.setPassword(oauth2LoginReq.getPassword() == null ? null : SecureUtil.md5(oauth2LoginReq.getPassword())) ;
        }
        Map<String, String> parameters = new HashMap<>();
        parameters.put(FieldUtil.getField(Oauth2LoginReq::getGrant_type).getName(), oauth2LoginReq.getGrant_type());
        parameters.put(FieldUtil.getField(Oauth2LoginReq::getClient_id).getName(), oauth2LoginReq.getClient_id());
        parameters.put(FieldUtil.getField(Oauth2LoginReq::getClient_secret).getName(), oauth2LoginReq.getClient_secret());
        parameters.putIfAbsent(FieldUtil.getField(Oauth2LoginReq::getRefresh_token).getName(), oauth2LoginReq.getRefresh_token());
        parameters.putIfAbsent(FieldUtil.getField(Oauth2LoginReq::getUsername).getName(), oauth2LoginReq.getUsername());
        parameters.putIfAbsent(FieldUtil.getField(Oauth2LoginReq::getPassword).getName(), oauth2LoginReq.getPassword());
        parameters.putIfAbsent("scopes", "all");
        //要提供UsernamePasswordAuthenticationToken参数，否则固定RequestMapping：/oauth/token才会被拦截生成UsernamePasswordAuthenticationToken然后被放行
        //传client_id，client_secret会先进行客户端登录生成客户端令牌，然后才根据username,password登录生成accessToken
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(oauth2LoginReq.getClient_id(), oauth2LoginReq.getClient_secret(), new HashSet<>());
        usernamePasswordAuthenticationToken.setDetails(parameters);

        OAuth2AccessToken oAuth2AccessToken = null;
        try {
            oAuth2AccessToken = tokenEndpoint.postAccessToken(usernamePasswordAuthenticationToken, parameters).getBody();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }

        if(oAuth2AccessToken==null){
            return ResponseVO.failed("登录失败");
        }

        //保存会话
        if (oAuth2AccessToken != null && !oAuth2AccessToken.isExpired() && StringUtils.isNotBlank(oAuth2AccessToken.getValue())) {
            OAuth2Authentication oAuth2Authentication = tokenStore.readAuthentication(oAuth2AccessToken);
            String userName = oAuth2Authentication.getName();
            //获取token唯一标识
            String jti = (String) oAuth2AccessToken.getAdditionalInformation().get(AuthConstant.JTI_KEY);
            //获取过期时间
            Date expiration = oAuth2AccessToken.getExpiration();
            long exp = expiration.getTime() / 1000;
            long currentTimeSeconds = System.currentTimeMillis() / 1000;
            if(authProperties.getSessionYn()) {
                redisService.set(AuthConstant.TOKEN_SESSIONLIST_PREFIX + userName, jti, (exp - currentTimeSeconds));
            }
        }

        Oauth2TokenDto oauth2TokenDto = Oauth2TokenDto.builder()
                .token(oAuth2AccessToken.getValue())
                .refreshToken(oAuth2AccessToken.getRefreshToken() == null ? null : oAuth2AccessToken.getRefreshToken().getValue())
                .expiresIn(oAuth2AccessToken.getExpiresIn())
                .tokenHead(AuthConstant.JWT_TOKEN_PREFIX).build();
        return ResponseVO.success(oauth2TokenDto);
    }

    @GetMapping("/publicKey")
    public Map<String, Object> getKey(HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        String string = request.getRequestURL().toString();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAKey key = new RSAKey.Builder(publicKey).build();
        return new JWKSet(key).toJSONObject();
    }


    @GetMapping("/md5")
    public String md5(String source) {
        return SecureUtil.md5(source);
        //return new BCryptPasswordEncoder().encode(source);
    }

    @GetMapping("/bCryptPasswordEncode")
    public String bCryptPasswordEncode(String source) {
        return new BCryptPasswordEncoder().encode(source);
    }


    @Operation(summary = "登出")
    @RequestMapping(value = "/logout", method = RequestMethod.POST)
    @ResponseBody
    public ResponseVO logout(@RequestHeader(value = HttpHeaders.AUTHORIZATION) String authHeader) {
        //获取token，去除前缀
        String token = authHeader.replace(OAuth2AccessToken.BEARER_TYPE, "").trim();
        // 解析Token
        OAuth2AccessToken oAuth2AccessToken = tokenStore.readAccessToken(token);

        if (oAuth2AccessToken != null && !oAuth2AccessToken.isExpired() && StringUtils.isNotBlank(oAuth2AccessToken.getValue())) {
            OAuth2Authentication oAuth2Authentication = tokenStore.readAuthentication(oAuth2AccessToken);
            String userName = oAuth2Authentication.getName();
            //获取token唯一标识
            String jti = (String) oAuth2AccessToken.getAdditionalInformation().get(AuthConstant.JTI_KEY);
            //获取过期时间
            Date expiration = oAuth2AccessToken.getExpiration();
            long exp = expiration.getTime() / 1000;
            long currentTimeSeconds = System.currentTimeMillis() / 1000;

            if(authProperties.getSessionYn()){
                //删除会话
                redisService.del(AuthConstant.TOKEN_SESSIONLIST_PREFIX + userName);
            }else{
                //token黑名单下线
                redisService.set(AuthConstant.TOKEN_BLACKLIST_PREFIX + jti, userName, (exp - currentTimeSeconds));
            }
            //删了token并不会下线,而且refreshtoken还可以继续使用
            tokenStore.removeAccessToken(oAuth2AccessToken);
        }


        return ResponseVO.success(null);
    }


    @Operation(summary = "踢下线")
    @RequestMapping(value = "/kick", method = RequestMethod.POST)
    @ResponseBody
    public ResponseVO kick(String userName) {
        if(authProperties.getSessionYn()) {
            //删除会话
            redisService.del(AuthConstant.TOKEN_SESSIONLIST_PREFIX + userName);
        }
        return ResponseVO.success(null);
    }


    @GetMapping("/aesEncode")
    public String aesEncode(String content) {
        // 加密为16进制表示
        return aes.encryptHex(content);
    }

    @GetMapping("/aesDecode")
    public String aesDecode(String encryptHex) {
        // 解密
        return aes.decryptStr(encryptHex);
    }


}
